﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.IO;
using System.Net;

namespace WFWS.Core
{
    public class BadRequestException : Exception
    {
        public BadRequestException(string param1)
        {
        }
    }
    /// <summary>
    /// maybe we need a More Beautiful parser
    /// TODO:Need to Static
    /// </summary>
    public class HttpRequestParser : IHttpRequestParser
    {
        #region IHttpRequestParser Members

        #region Fileds
        private int _bodyBytesLeft = 0;
        private string _currentHeaderName;
        private string _currentHeaderValue;
        private BodyEventArgs _bodyEventArgs = new BodyEventArgs();
        Socket _socket;
        byte[] _buffer = new byte[4096];
        Stream _stream;
        HttpClientRequest _request;
        RequestParserState _currentState;
        #endregion


        public HttpRequestParser(Socket socket)
        {
            CurrentState = RequestParserState.FirstLine;
            _request = new HttpClientRequest(socket);
            _socket = socket;
            _stream = new NetworkStream(socket);
            _request.RemoteEndPoint = (IPEndPoint)socket.RemoteEndPoint;
        }


        public RequestParserState CurrentState
        {
            get { return _currentState; }
            set { _currentState = value; }
        }

        #region Methods
        public void ParseAndQueueRequest()
        {
            //Queue the Item when the process of parsing finished in the OnComplete method
            _stream.BeginRead(_buffer, 0, _buffer.Length, OnStreamRead, null);
        }

        private void OnStreamRead(IAsyncResult ar)
        {
            int bytes = _stream.EndRead(ar);
            if (bytes > 0)
            {
                Parse(_buffer, 0, bytes);
            }
        }
        #region Parse
        ///TODO:
        ///Big problem: !!!! how to test?
        public int Parse(byte[] buffer, int offset, int length)
        {
            if (CurrentState == RequestParserState.Body)
            {
                //Continue to parse the body
                return AddToBody(buffer, 0, length);
            }

            int currentLine = 1;
            int startPos = -1;
            int endBufferPos = offset + length;
            int handledBytes = 0;

            //Continue to parse
            if (CurrentState == RequestParserState.HeaderValue)
                startPos = 0;

            int endOfBufferPos = offset + length;
            for (int currentPos = offset; currentPos < endOfBufferPos; currentPos++)
            {
                var ch = (char)buffer[currentPos];
                char nextCh = endOfBufferPos > currentPos + 1 ? (char)buffer[currentPos + 1] : char.MinValue;

                if (ch == '\r')
                    currentLine++;

                switch (CurrentState)
                {
                    //TODO: no bad request check here:
                    case RequestParserState.FirstLine:
                        {
                            if (Char.IsLetterOrDigit(ch) && startPos == -1)
                                startPos = currentPos;
                            if (startPos != -1 && (ch == '\r' || ch == '\n'))
                            {
                                int size = GetLineBreakSize(buffer, currentPos);
                                ParseFirstLine(Encoding.UTF8.GetString(buffer, startPos, (currentPos - startPos)));
                                CurrentState = CurrentState + 1;
                                currentPos += size - 1;
                                handledBytes = currentPos;
                                startPos = -1;//parse next part
                            }
                        }
                        break;
                    case RequestParserState.HeaderName:
                        {
                            //Empty Head
                            if (ch == '\r' || ch == '\n')
                            {
                                currentPos += GetLineBreakSize(buffer, currentPos);
                                if (_bodyBytesLeft == 0)
                                {
                                    OnRequestCompleted();
                                    handledBytes = currentPos;
                                    //go back to FirstLine
                                    CurrentState = RequestParserState.FirstLine;
                                    return currentPos;
                                }
                                else
                                {
                                    //go to body:
                                    CurrentState = RequestParserState.Body;
                                    if (currentPos + 1 < endBufferPos)
                                    {
                                        handledBytes = currentPos;
                                        return AddToBody(buffer, currentPos, endBufferPos - currentPos);
                                    }
                                }
                                return currentPos;
                            }
                            if (ch == ':' || Char.IsWhiteSpace(ch))
                            {
                                if (startPos == -1)
                                {
                                    throw new BadRequestException("No Header name!");
                                }

                                _currentHeaderName = Encoding.UTF8.GetString(buffer, startPos, currentPos - startPos);
                                startPos = -1;
                                CurrentState = CurrentState + 1;
                                if (ch == ':')
                                    CurrentState = CurrentState + 1;
                            }
                            else if (startPos == -1)
                                startPos = currentPos;
                            else if (!char.IsLetterOrDigit(ch) && ch != '-' && ch != '_')
                            {
                                throw new BadRequestException("Invalid character in header name on line " + currentLine);
                            }
                            if (startPos != -1 && currentPos - startPos > 200)
                            {
                                throw new BadRequestException("Invalid header name on line " + currentLine);
                            }
                        }
                        break;
                    case RequestParserState.AfterName:
                        if (ch == ':')
                            CurrentState = CurrentState + 1;
                        handledBytes = currentPos + 1;
                        break;
                    case RequestParserState.Between:
                        {
                            //ignore the SP TAB and CRLF:
                            if (ch == ' ' || ch == '\t')
                                continue;
                            int newLineSize = GetLineBreakSize(buffer, currentPos);
                            if (newLineSize > 0 && newLineSize + currentPos < endBufferPos
                                && Char.IsWhiteSpace((char)buffer[currentPos + newLineSize]))
                            {
                                //new line start with SP
                                currentPos++;
                                continue;
                            }
                            startPos = currentPos;
                            CurrentState = CurrentState + 1;
                            handledBytes = currentPos;
                            continue;
                        }
                        break;
                    case RequestParserState.HeaderValue:
                        {
                            ///!!!!TODO:
                            ///if the request dose not end with the \r or \n
                            ///we'll miss the last line
                            if (ch != '\n' && ch != '\r')
                                continue;
                            int newLineSize = GetLineBreakSize(buffer, currentPos);
                            //if(startPos == -1) continue;
                            _currentHeaderValue = Encoding.UTF8.GetString(buffer, startPos, currentPos - startPos);
                            OnHeader(_currentHeaderName, _currentHeaderValue);
                            startPos = -1;
                            CurrentState = RequestParserState.HeaderName;
                            _currentHeaderValue = "";
                            _currentHeaderName = "";
                            handledBytes = currentPos + 1;
                            currentPos += newLineSize > 1 ? 1 : 0;

                            //only headers?
                            if (currentPos < buffer.Length)
                            {
                                bool onlyHeader = true;
                                for (int i = currentPos; i < buffer.Length; i++)
                                {
                                    if (!Char.IsWhiteSpace((char)buffer[i]))
                                    {
                                        onlyHeader = false;
                                        break;
                                    }
                                }
                                if (onlyHeader)
                                    OnRequestCompleted();
                            }
                            else
                            {
                                OnRequestCompleted();
                            }
                        }
                        break;
                    default:
                        break;
                }
            }
            return handledBytes;
        }

        private void ParseFirstLine(string value)
        {
            string method = "", path = "", version = "";

            //get the http method
            int pos = value.IndexOf(' ');
            if (pos > 0 && pos + 1 <= value.Length)
            {
                method = value.Substring(0, pos).ToUpper();
            }

            //get the request path
            int oldpos = pos + 1;
            pos = value.IndexOf(' ', oldpos);
            path = value.Substring(oldpos, pos - oldpos);

            //get the http version:
            version = value.Substring(pos + 1);

            RequestLineReceived(this, new RequestLineEventArgs(method, path, version));
        }

        public static HttpCookieCollection ParseCookie(string cookieValue)
        {
            HttpCookieCollection cookieCollection = new HttpCookieCollection();
            string[] pairs = cookieValue.Split(new char[] { ';' });
            foreach (var pair in pairs)
            {
                string[] kv = pair.Split(new char[] { '=' });
                if (kv.Length == 2)
                {
                    HttpCookie cookie = new HttpCookie(kv[0], kv[1]);
                    cookieCollection.Set(cookie);
                }
            }
            return cookieCollection;
        }

        public static HttpInputCollection ParseQueryString(string url)
        {
            HttpInputCollection collection = new HttpInputCollection();
            var startIndex = 0;

            if (url.IndexOf('?') > 0)
                startIndex = url.LastIndexOf('?');

            string[] pairs = url.Substring(startIndex).Split(new char[] { '&' });
            foreach (var pair in pairs)
            {
                string[] kv = pair.Split(new char[] { '=' });
                if (kv.Length == 2)
                {
                    HttpInput input = new HttpInput(kv[0], kv[1]);
                    collection.Set(kv[0], input);
                }
            }
            return collection;
            /*
            var startIndex = 0;
            var actionStartIndex = -1;
            bool parseName = true;
            
            if (url.IndexOf('?') > 0)
                startIndex = url.LastIndexOf('?');

            for (int i = startIndex; i < url.Length; i++)
            {
                var ch = url[i];

                if (parseName)
                {
                }
            }
            throw new NotImplementedException();
            */

        }

        private int GetLineBreakSize(byte[] buffer, int currentPos)
        {
            if (buffer[currentPos] != '\r' && buffer[currentPos] != '\n')
                return 0;

            if (buffer[currentPos] == '\n' && (buffer.Length == currentPos + 1 || buffer[currentPos + 1] != '\r'))
                return 1;

            if (buffer[currentPos] == '\r' && buffer.Length > currentPos + 1 && buffer[currentPos + 1] == '\n')
                return 2;

            if (buffer[currentPos] == '\n' && buffer.Length > currentPos + 1 && buffer[currentPos + 1] == '\r')
                return 2;

            throw new BadRequestException("bad line break!");
        }



        private int AddToBody(byte[] buffer, int offset, int length)
        {
            int bytesUsed = length > _bodyBytesLeft ? _bodyBytesLeft : length;
            _bodyEventArgs.Buffer = buffer;
            _bodyEventArgs.Count = bytesUsed;
            _bodyEventArgs.Offset = offset;
            BodyBytesReceived(this, _bodyEventArgs);

            _bodyBytesLeft -= bytesUsed;

            if (_bodyBytesLeft == 0)
            {
                OnRequestCompleted();
                Clear();
            }
            return offset + bytesUsed;
        }
        private void Clear()
        {
            _bodyBytesLeft = 0;
            _currentHeaderName = "";
            _currentHeaderValue = "";
            CurrentState = RequestParserState.FirstLine;
        }


        #endregion
        #endregion

        #region Events

        private void OnHeader(string currentHeaderName, string currentHeaderValue)
        {
            _request.Headers.Set(new HttpHeader(currentHeaderName, currentHeaderValue));
            if (currentHeaderName == "Cookie")
            {
                var cookies = HttpRequestParser.ParseCookie(currentHeaderValue);
                _request.Cookies = cookies;
            }
            HeaderReceived(this, new HeaderEventArgs(currentHeaderName, currentHeaderValue));
        }

        //Request Enqueue
        private void OnRequestCompleted()
        {
            HttpRequestManager.QueueWorkRequest(_request);

            RequestCompleted(this, EventArgs.Empty);
        }

        private void OnRequestLine(string method, string path, string version)
        {
            _request.Method = method;
            _request.HttpVersion = version;
            _request.Path = path;

            RequestLineReceived(this, new RequestLineEventArgs(method, path, version));
        }

        public event EventHandler RequestCompleted = delegate { };

        public event EventHandler<BodyEventArgs> BodyBytesReceived = delegate { };

        public event EventHandler<RequestLineEventArgs> RequestLineReceived = delegate { };

        public event EventHandler<HeaderEventArgs> HeaderReceived = delegate { };

        #endregion

        /*
        private void Callback(IAsyncResult ar)
        {
            int bytes = _stream.EndRead(ar);
            if (bytes > _buffer.Length)
                throw new Exception("too larger HeaderReceived");

            string value = Encoding.ASCII.GetString(_buffer);
            string method = "";
            string path = "";
            int pos = value.IndexOf(' ');
            //get the http method
            if (pos < 0 || pos + 1 >= value.Length)
            {
                method = value.Substring(0, pos).ToUpper();
            }
            //get the request path
            int oldpos = pos + 1;
            pos = value.IndexOf(' ', oldpos);
            path = value.Substring(oldpos, pos - oldpos);

            //get the http version:
            string version = value.Substring(pos + 1);
            RequestLineEventArgs ra = new RequestLineEventArgs(method, path, version);
            RequestLineReceived(this, ra);
            CurrentState = RequestParserState.HeaderValue;
        }
*/

        #endregion
    }
}
